package simulacao.modelo.veiculos;

import javafx.scene.CustomNode;
import javafx.scene.image.ImageView;
import javafx.animation.transition.ParallelTransition;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import simulacao.util.Util;
import simulacao.modelo.pistas.Pista;
import javafx.scene.shape.Rectangle;
import javafx.util.Math;
import simulacao.Config;
import javafx.scene.shape.Polygon;
import javafx.scene.Node;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import simulacao.SimulationView;
import javafx.scene.shape.Shape;
import javafx.animation.transition.PathTransition;
import simulacao.util.SaidaPersonalizada.*;
import java.lang.System;

// Tipo do Veiculo
public-read def TIPO_CARRO = 1;
public-read def TIPO_CAMINHAO = 2;

public class Veiculo extends CustomNode {


    /******************************************************************

    Atributos pertinentes a representação real de um veículo

     ******************************************************************/

    // Indica se o veiculo atual é um carro ou um caminhão
    public-init var tipoVeiculo: Integer;

    // A largura do veiculo
    public-init var larguraVeiculo: Number;
    // O comprimento do veiculo
    public-init var comprimentoVeiculo: Number;

    // Pista na qual o veículo se encontra no momento
    public var pista: Pista;

    // Indice da faixa de rodagem que o veículo está
    public-init var faixaDeRodagemAtual: Integer;

    // A velocidade atual do veiculo
    public var velocidadeAtual: Number = 0 on replace {
        if (velocidadeAtual < 0) {
            velocidadeAtual = 0
        }
    };
    // A velocidade desejada do veiculo em pista livre (sua velocidade máxima)
    public var velocidadeEmPistaLivre: Number = 0 on replace {
        if (velocidadeEmPistaLivre < 0) {
            velocidadeEmPistaLivre = 0
        }
    };

    // A velocidade que o veículo terá caso ele esteja em uma região de subida
    public-read var velocidadeEmSubida: Number = 0;

    // A velocidade "alvo" do veiculo naquele momento. Baseado em sua velocidade atual, ele consegue determinar
    // o quanto precisa aumentar ou diminuir sua velocidade para atingir o valor desejado.
    // Por exemplo: Se ele estiver atras de um veiculo mais lento, sua velocidade "alvo" será a mesma que a
    // desse veiculo. Se ele estiver em pista livre ou em subida será, respectivamente, uma das duas.
    public var velocidadeAlvo: Number = 0 on replace {
        if (velocidadeAlvo > velocidadeEmPistaLivre) {
            velocidadeAlvo = velocidadeEmPistaLivre;
        } else if (velocidadeAlvo < 0){
            velocidadeAlvo = 0;
        }

    };

    // Equivale a uma aceleração/desaceleração que o veiculo terá que fazer para atingir sua velocidade alvo
    public-read var deltaVelocidade: Number = 0;
                     
    // Essa fórmula calcula a distância de segurança que o veiculo deve manter para o da frente
    public  var distanciaDeSegurancaRelativaAVelocidadeAtual: Number
//       = bind 4*Math.pow(1 + Math.tanh((velocidadeAtual / Config.VELOCIDADE_MAXIMA_CARRO)), 2);
       = bind 9*(1 + Math.tanh((3 * velocidadeAtual / Config.VELOCIDADE_MAXIMA_CARRO) - 1.5));

    // Indice que quantifica o quao agressivo o veiculo será, ou seja, o quanto ele vai
    // tentar trocar de faixa quando estiver sendo retido por outro veiculo mais lento
    // Varia de 0 (nunca troca de faixa) a 10 (sempre troca de faixa)
    public var indiceDeAgressividade : Integer = 0 on replace {
        if (indiceDeAgressividade < 0) {
            indiceDeAgressividade = 0;
        } else if (indiceDeAgressividade > Config.INDICE_AGRESSIVIDADE_MAXIMO){
            indiceDeAgressividade = Config.INDICE_AGRESSIVIDADE_MAXIMO;
        }
    };

/*******************************************************

    Atributos utilizados para adequacao ao framework

********************************************************/

    // Numero de identificação do veiculo durante a simulação. Importante para tarefas
    // de detecção de colisão, dentre outras coisas.
    public-init var idVeiculo: Integer;

    // A imagem de cada veiculo
    var imagem: ImageView;

    public var animacaoDoVeiculo : PathTransition;

    // Define a velocidade com que a animacao será exibida na tela. Definida em funcao da velocidade de cada veiculo.
    // Mexendo nesse valor é que efetivamente conseguimos dar a impressão de acelerar ou desacelerar os veiculos.
    public var rateRelativaVelocidadeAtual: Double = bind velocidadeAtual / Config.VELOCIDADE_MAXIMA_CARRO;

    // Quantidade de vezes que o veiculo NÃO colidiu frontalmente para determinar com segurança
    // que ele de fato NÃO ESTÁ colidindo
    public var quantidadeNaoColisoesFrontais : Integer = 0;

    // Quantidade de vezes que o veiculo COLIDIU frontalmente para determinar com segurança
    // que ele de fato ESTÁ colidindo
    public var quantidadeColisoesFrontais : Integer = 0;
    
    // Indica se o veiculo está nesse momento com algum outro veiculo em sua faixa lateral.
    public var quantidadeColisoesLaterais : Integer = 0;
    public var quantidadeNaoColisoesLaterais : Integer = 0;

    // Quantas vezes o veiculo tentou trocar de faixa sem sucesso. 
    // Essa variável é zerada assim que o veículo consegue trocar de faixa.
    public var quantidadeTentativasTrocarDeFaixa : Integer = 0;

    // Indica se o veiculo deseja trocar de faixa de rodagem assim que possivel
    public var trocarDeFaixaAssimQuePossivel : Boolean = false;

    // Bounding Box que verifica se uma colisão frontal está iminente, ou seja, se um
    // veiculo esta proximo do veiculo na sua frente. O comprimento desse retângulo é
    // deteriminado pela fórmula de distância de segurança.
    public var boundingBoxFrontal: Shape;


    // Bounding Box que verifica se uma colisão lateral está ocorrendo, ou seja, se um
    // veiculo esta na faixa de rodagem ao seu lado. Usado para determinar se um veiculo
    // pode ou não trocar de faixa.
    public var boundingBoxLateral: Shape;

    // Bounding Box que deve ficar ao redor do veiculo e que verifica se uma colisão
    // diretamente com veiculo está ocorrendo
    public var boundingBoxVeiculo: Shape;

    // Veiculo com o qual esta sobrepostos. -1  indica que não há sobreposição
    public var idVeiculoSobreposto : Integer = -1;

    // Define se o veiculo esta em processo de ajuste de velocidade (Acelerando ou Desacelerando)
    public var ajustandoVelocidade : Boolean = false;

    // Quantidade de loops para que um veiculo fique recém adicionado apareça na simulação.
    // Quando um veiculo eh adicionado ele nao deve aparecer na hora pois pode estar se soprepondo a outro
    // Essa quantidade de loops "dá um tempo" para o veiculo de ajustar na simulação
    public var quantidadeLoopsParaFicarVisivel = 10;

    // Dado um valor de velocidade, essa função atribuiu um valor semi-randomico para a velocidade do veiculo.
    // É importante para que cada veiculo tenha uma velocidade diferente, porém dentro de uma faixa estipulada.
    // Ou seja, a velocidade atribuida ao veiculo estará no intervalo de 80% a 120% da velocidade desejada.
    public function setVelocidadeSemiRandomica(velocidade: Number) {
        velocidadeEmPistaLivre = velocidade * (Util.random(0.41) + 0.8);
        inicializaComValoresDefault();
    }

    public function setVelocidadeEmSubida(velocidadeMaxima : Number) {
        if (tipoVeiculo == TIPO_CARRO){
            // A velocidade do veiculo tipo carro será entre 40% e 60% de sua velocidade em pista livre
            velocidadeEmSubida = velocidadeMaxima * (Util.random(0.21) + 0.4);
        } else if (tipoVeiculo == TIPO_CAMINHAO){
            // A velocidade do veiculo tipo caminhao será entre 30% e 50% de sua velocidade em pista livre
            velocidadeEmSubida = velocidadeMaxima * (Util.random(0.21) + 0.3);
        } else {
            System.err.println("Tipo veiculo inválido {tipoVeiculo}");
        }
        
    }

    public function setVelocidadeAtual(novaVelocidade : Number) {

        if (novaVelocidade == 0){
            animacaoDoVeiculo.pause();
            velocidadeAtual = 0.00000001;
            escreverSaida("Animacao do Veiculo {idVeiculo} foi pausada porque sua velocidade atingiu zero!");

        } else{

            if (animacaoDoVeiculo.paused){
                animacaoDoVeiculo.play();
                escreverSaida("Animacao do Veiculo {idVeiculo} voltou a ser executada porque sua velocidade foi alterada!");
            }

            velocidadeAtual = novaVelocidade;
        }
    }

    public function inicializaComValoresDefault() {
        velocidadeAtual = velocidadeEmPistaLivre;
        velocidadeAlvo = velocidadeEmPistaLivre;
        setVelocidadeEmSubida(velocidadeEmPistaLivre);
        deltaVelocidade = 0;
        ajustandoVelocidade = false;
        idVeiculoSobreposto = -2;
        visible = false;
    }


    // Essa função tenta trocar o veiculo de faixa
    public function verificarTrocaDeFaixaDeRodagem() {

        if (trocarDeFaixaAssimQuePossivel){

            escreverSaida("Veiculo {idVeiculo} ira tentar trocar de faixa...");
            
            if (trocarDeFaixaDeRodagem()){
                escreverSaida("Veiculo {idVeiculo} tentou E coseguiu trocar de faixa...  =D");
                trocarDeFaixaAssimQuePossivel = false;
                quantidadeTentativasTrocarDeFaixa = 0;
                
                // Para nunca zerar o indice de agressividade de um veiculo
                // Caso atigisse zero o veiculo nunca mais iria trocar de faixa
                // Embora possam existir veiculos que já sejam criados com esse índice igual a zero
                if (indiceDeAgressividade / 2 == 0){
                    indiceDeAgressividade = 1;
                } else{
                    indiceDeAgressividade /= 2;
                }

            } else {
                escreverSaida("Veiculo {idVeiculo} tentou mas NAO coseguiu trocar de faixa...  =(");
                quantidadeTentativasTrocarDeFaixa++;

                if (quantidadeTentativasTrocarDeFaixa mod Config.QUANTIDADE_DE_TENTATIVAS_TROCA_DE_FAIXA_PARA_INCREMENTAR_INDICE_AGRESSIVIDADE == 0){
                    escreverSaida("Veiculo {idVeiculo} aumentou seu indice de Agressividade pois esta tentando trocar de faixa a {quantidadeTentativasTrocarDeFaixa} loops");
                    indiceDeAgressividade++;
                }
            }
        }
    }


    // Essa função verifica se o veiculo pode mudar de faixa naquele momento, ou seja,
    // se ele nao esta colidindo lateralmente com nenhum outro veiculo. Caso seja possivel
    // ocorre a mudancao de faixa.
    // A animacao do veiculo ocorre sobre um path que esta ligado por "bind" ao valor do
    // atributo faixaAtual. Alterando esse valor, o veiculo passa automaticamente para a nova faixa.
    public function trocarDeFaixaDeRodagem() : Boolean {

        /*************************************** Verifica a colisao lateral *************************************************/
        var lateralBoundsVeiculoAtual = this.boundingBoxLateral.localToScene(boundingBoxLateral.boundsInLocal);
        quantidadeColisoesLaterais = 0;

        for (veiculoComparado in SimulationView.cenarioCorrente.veiculos) {

            if (idVeiculo != veiculoComparado.idVeiculo and faixaDeRodagemAtual != veiculoComparado.faixaDeRodagemAtual) {

                var lateralBoundsVeiculoComparado = veiculoComparado.boundingBoxLateral.localToScene(veiculoComparado.boundingBoxLateral.boundsInLocal);

                if (lateralBoundsVeiculoAtual.intersects(lateralBoundsVeiculoComparado)) {
                    quantidadeColisoesLaterais++;
                    quantidadeNaoColisoesLaterais = 0;
                    break;
                }
            }
        }

        if (quantidadeColisoesLaterais == 0) {
            faixaDeRodagemAtual = (faixaDeRodagemAtual + 1) mod pista.QUANTIDADE_DE_FAIXAS;
            var parTransition = ParallelTransition {
                    node: this
                    interpolator:Interpolator.LINEAR
                    keyFrames: [
                        KeyFrame {
                            time : 0s,
                                values: [this.translateX => this.translateX, this.translateY => this.translateY]
                            },
                            KeyFrame {
                                time: 0.3s,
                                values: [this.translateX => this.translateX, this.translateY => this.translateY]
                            }
                        ]
                    }
            parTransition.play();
        }

        return quantidadeColisoesLaterais == 0;
    }

    public function verificarAjusteDeVelocidade() {


        if (quantidadeColisoesFrontais > 0 and 
            quantidadeColisoesFrontais mod Config.QUANTIDADE_COLISOES_SEGUIDAS_PARA_REDUZIR_VELOCIDADE == 0) {

            velocidadeAlvo *= Config.PORCENTAGEM_REDUCAO_VELOCIDADE;
            escreverSaida("Veiculo {idVeiculo} Reduziu a {Config.PORCENTAGEM_REDUCAO_VELOCIDADE*100}% sua velocidade pois estava preso atras de um veiculo.")
        }

        // Se a diferenca de velocidade for pouca, ajusto instantaneamente
        // ou
        // Se o delta for muito pequeno, deve-se reduzir a velocidade do veiculo imediatamente
        if (Math.abs(velocidadeAlvo - velocidadeAtual) <= Config.DIFERENCA_MINIMA_VELOCIDADE_AJUSTE_IMEDIATO + Math.abs(deltaVelocidade) or
            deltaVelocidade < Config.DELTA_MINIMO_PARA_PERMITIR_AJUSTE_PROGRESSIVO){

            setVelocidadeAtual(velocidadeAlvo);
            
            deltaVelocidade = 0;
            ajustandoVelocidade = false;
            escreverSaida("Veiculo {idVeiculo} atingiu a velocidade alvo!");

        // Caso contrario, faço um ajuste suave...
        // O decremento necessario na velocidade atual do veiculo depende da diferenca das velocidades
        // dele proprio e do veiculo que esta na frente (representado pela sua velocidade delta),
        // mas so deve ser calculado uma vez e deve ser utilizado até alcançar a velocidade desejada.
        } else if (ajustandoVelocidade and isDeltaVelocidadeValido()) {

            velocidadeAtual += deltaVelocidade;

        // Caso contrario, quer dizer que precisamos calcular o delta
        } else {

            ajustandoVelocidade = true;

            // O carro esta muito devagar e precisa acelerar (Delta sera positivo)
            if (velocidadeAtual < velocidadeAlvo){
                deltaVelocidade = (velocidadeAlvo - velocidadeAtual) / Config.QUANTIDADE_LOOPS_ACELERACAO;

            // Caso contrario, precisa desacelerar (Delta sera negativo)
            } else {
                deltaVelocidade = (velocidadeAlvo - velocidadeAtual) / Config.QUANTIDADE_LOOPS_DESACELERACAO;
            }
        }
    }

    function isDeltaVelocidadeValido() : Boolean {
        return
        // O carro deve desacelerar e delta é negativo  --> OK
        (velocidadeAtual > velocidadeAlvo and deltaVelocidade < 0)
         or
        // O carro deve acelerar e delta é positivo  --> OK
        (velocidadeAtual < velocidadeAlvo and deltaVelocidade > 0);
    }

    public function isDesacelerando() : Boolean {
        return ajustandoVelocidade and deltaVelocidade < 0;
    }

    public function isAcelerando() : Boolean {
        return ajustandoVelocidade and deltaVelocidade > 0;
    }

    public function isAntecipandoColisao(): Boolean {
        return quantidadeColisoesFrontais > 0;
    }

    public function isNOTAntecipandoColisao(): Boolean {
        // Artificio utilizado para dar uma margem de seguranca para nao deteccao de colisao.
        return quantidadeNaoColisoesFrontais >= Config.QUANTIDADE_MINIMA_NAO_COLISOES_SEGUIDAS;
    }

    override public function create(): Node {

        if(tipoVeiculo == TIPO_CARRO){
            setVelocidadeSemiRandomica(Config.VELOCIDADE_MAXIMA_CARRO);
            imagem = ImageView {image : Util.makeImage(Config.imagensCarro[Util.random(Config.imagensCarro.size())])};

        } else if(tipoVeiculo == TIPO_CAMINHAO){
            setVelocidadeSemiRandomica(Config.VELOCIDADE_MAXIMA_CAMINHAO);
            imagem = ImageView {image : Util.makeImage(Config.imagensCaminhao[Util.random(Config.imagensCaminhao.size())])};
        }

        larguraVeiculo = imagem.image.height;
        comprimentoVeiculo = imagem.image.width;

        boundingBoxFrontal = Polygon {
                            points: bind [ 0,0, distanciaDeSegurancaRelativaAVelocidadeAtual * 4, -11,  distanciaDeSegurancaRelativaAVelocidadeAtual * 4, 11 ];
                            layoutX: comprimentoVeiculo + 15
                            layoutY: larguraVeiculo/2
                            fill: bind if(quantidadeColisoesFrontais > 0) then Color.RED else Color.GREEN
                            opacity: bind if(Config.EXIBIR_BOUNDING_BOX_DETECTAR_COLISAO_FRONTAL) 0.5 else 0.0
                        };

//        boundingBoxLateral = Polygon {
//                            points: [ 0,0   ,  -6,35   ,  6,35 ,
//                                     -6,-35 ,   6,-35  ,  0,0  ,
//                                     comprimentoVeiculo,0        ,  -6 + comprimentoVeiculo,35   ,  6 + comprimentoVeiculo,35  ,
//                                     -6 + comprimentoVeiculo,-35 ,   6 + comprimentoVeiculo,-35  ,  comprimentoVeiculo,0
//                                     ];
        boundingBoxLateral = Rectangle {
                            height: larguraVeiculo + 50
                            width: comprimentoVeiculo + 100
                            layoutX: -50
                            layoutY: -25
                            fill: bind if(quantidadeNaoColisoesLaterais > 10) then Color.LIGHTBLUE else Color.PINK
                            opacity: bind if(Config.EXIBIR_BOUNDING_BOX_DETECTAR_COLISAO_LATERAL) 0.5 else 0.0
                        };

        boundingBoxVeiculo = Rectangle{
                            height: larguraVeiculo + 10
                            width: comprimentoVeiculo + 30
                            layoutX: -15
                            layoutY: -5
                            arcHeight: 10
                            arcWidth:  10
                            fill: bind if(SimulationView.cenarioCorrente.veiculoSelecionado.idVeiculo == idVeiculo) Color.YELLOW 
                                       else if (SimulationView.cenarioCorrente.veiculoRecemAdicionado.idVeiculo == idVeiculo) Color.DARKSLATEGRAY
                                       else Color.GHOSTWHITE
                            opacity: 0.4
                            visible: bind Config.EXIBIR_BOUNDING_BOX_VEICULO
                        };

        return Group {content: [ boundingBoxLateral, boundingBoxVeiculo, imagem, boundingBoxFrontal, Text{content:String.valueOf(idVeiculo) layoutX: 6}]}
    }

    override public function toString() : String {
        return if (tipoVeiculo == TIPO_CARRO) "Carro {idVeiculo}" else "Caminhão {idVeiculo}"
    }

    public function imprimirDados() : String {
        return {"\n ******************** Veiculo {idVeiculo} *******************"
                "\nVelocidade Atual           {velocidadeAtual}"
                "\nVelocidade Alvo            {velocidadeAlvo}"
                "\nVelocidade Em pista livre  {velocidadeEmPistaLivre}"
                "\nVelocidade Em subida       {velocidadeEmSubida}"
                "\nVelocidade Delta           {deltaVelocidade}"
                "\nÍndice de Agressividade    {indiceDeAgressividade}"
                "\nTrocar de Faixa?           {trocarDeFaixaAssimQuePossivel}"
                "\nAjustando Velocidade       {ajustandoVelocidade}"
                "\nDistancia de Seguranca     {distanciaDeSegurancaRelativaAVelocidadeAtual}"
                "\nQtd Colisoes Frontais      {quantidadeColisoesFrontais}"
                "\nQtd NAO Colisoes Frontais  {quantidadeNaoColisoesFrontais}"
                "\n********************************************************"
                }
    }
}
